package org.web3j.crypto.seele;

import org.web3j.crypto.Credentials;
import org.web3j.crypto.Hash;
import org.web3j.rlp.RlpEncoder;
import org.web3j.rlp.RlpList;
import org.web3j.rlp.RlpString;
import org.web3j.rlp.RlpType;
import org.web3j.utils.Bytes;
import org.web3j.utils.Numeric;

import java.util.ArrayList;
import java.util.List;

/**
 * Create RLP encoded transaction, implementation as per p4 of the
 * <a href="http://gavwood.com/paper.pdf">yellow paper</a>.
 */
public class TransactionEncoder {

    public static Transaction signSeeleMessage(RawTransaction rawTransaction, Credentials credentials) {

        //设置交易发起方地址
        rawTransaction.setFromAddress(credentials.getAddress());
        //rawTransaction.setFromAddress("0xa61e5b0b30e91c4ae10dda3a6ddeb9d9d35ebfe1");

        //第一个数据节点(Data)
        Transaction tx = new Transaction(rawTransaction);
        //RLP编码
        byte[] encodedTransaction = encode(rawTransaction);
        //Hash 签名参数
        byte[] hash = Hash.sha3(encodedTransaction);
        //第二个节点数据(Hash)
        tx.Hash = Numeric.toHexString(hash);
        //私钥 签名参数
        Sign.SignatureData signatureData = Sign.signMessage(
                hash, credentials.getEcKeyPair(), false);

        byte[] v = new byte[1];
        v[0] = signatureData.getV();

        byte[] sign = byteMergerAll(signatureData.getR(), signatureData.getS(), v);

        //第三个数据节点(Signature)
        tx.Signature = new Transaction.Signature(sign);

        return tx;
    }

    /**
     * 合并byte数组
     */
    public static byte[] unitByteArray(byte[] byte1, byte[] byte2) {
        byte[] unitByte = new byte[byte1.length + byte2.length];
        System.arraycopy(byte1, 0, unitByte, 0, byte1.length);
        System.arraycopy(byte2, 0, unitByte, byte1.length, byte2.length);
        return unitByte;
    }

    /**
     * 合并数组
     *
     * @param values
     * @return
     */
    private static byte[] byteMergerAll(byte[]... values) {
        int length_byte = 0;
        for (int i = 0; i < values.length; i++) {
            length_byte += values[i].length;
        }
        byte[] all_byte = new byte[length_byte];
        int countLength = 0;
        for (int i = 0; i < values.length; i++) {
            byte[] b = values[i];
            System.arraycopy(b, 0, all_byte, countLength, b.length);
            countLength += b.length;
        }
        return all_byte;
    }

    public static byte[] signMessage(RawTransaction rawTransaction, Credentials credentials) {

        byte[] encodedTransaction = encode(rawTransaction);
        Sign.SignatureData signatureData = Sign.signMessage(
                encodedTransaction, credentials.getEcKeyPair());
        return encode(rawTransaction, signatureData);
    }

    public static byte[] signMessage(
            RawTransaction rawTransaction, byte chainId, Credentials credentials) {
        byte[] encodedTransaction = encode(rawTransaction, chainId);
        Sign.SignatureData signatureData = Sign.signMessage(
                encodedTransaction, credentials.getEcKeyPair());

        Sign.SignatureData eip155SignatureData = createEip155SignatureData(signatureData, chainId);
        return encode(rawTransaction, eip155SignatureData);
    }

    public static Sign.SignatureData createEip155SignatureData(
            Sign.SignatureData signatureData, byte chainId) {
        byte v = (byte) (signatureData.getV() + (chainId << 1) + 8);

        return new Sign.SignatureData(
                v, signatureData.getR(), signatureData.getS());
    }

    /**
     * 1. 编码数据
     *
     * @param rawTransaction
     * @return
     */
    public static byte[] encode(RawTransaction rawTransaction) {
        return encode(rawTransaction, null);
    }

    public static byte[] encode(RawTransaction rawTransaction, byte chainId) {
        Sign.SignatureData signatureData = new Sign.SignatureData(
                chainId, new byte[]{}, new byte[]{});
        return encode(rawTransaction, signatureData);
    }

    /**
     * 2.编码数据&私钥
     *
     * @param rawTransaction
     * @param signatureData
     * @return
     */
    private static byte[] encode(RawTransaction rawTransaction, Sign.SignatureData signatureData) {

        //组建数据结构
        List<RlpType> values = asRlpValues(rawTransaction, signatureData);

        //构建对象
        RlpList rlpList = new RlpList(values);

        //编码
        return RlpEncoder.encode(rlpList);
    }

    /**
     * TODO
     *
     * @param rawTransaction
     * @param signatureData
     * @return
     */
    static List<RlpType> asRlpValues(
            RawTransaction rawTransaction, Sign.SignatureData signatureData) {

        List<RlpType> result = new ArrayList<RlpType>();

        String from = rawTransaction.getFrom();

        if (from != null && from.length() > 0) {
            result.add(RlpString.create(Numeric.hexStringToByteArray(from)));
        } else {
            result.add(RlpString.create(""));
        }

        // an empty to address (contract creation) should not be encoded as a numeric 0 value
        String to = rawTransaction.getTo();

        if (to != null && to.length() > 0) {
            // addresses that start with zeros should be encoded with the zeros included, not
            // as numeric values
            result.add(RlpString.create(Numeric.hexStringToByteArray(to)));
        } else {
            result.add(RlpString.create(""));
        }

        //Amount
        result.add(RlpString.create(rawTransaction.getValue()));

        result.add(RlpString.create(rawTransaction.getNonce()));
        result.add(RlpString.create(rawTransaction.getGasPrice()));
        result.add(RlpString.create(rawTransaction.getGasLimit()));

        //时间戳
        result.add(RlpString.create(rawTransaction.getTimestamp()));

        // value field will already be hex encoded, so we need to convert into binary first
        byte[] data = Numeric.hexStringToByteArray(rawTransaction.getData());
        result.add(RlpString.create(data));

        if (signatureData != null) {
            result.add(RlpString.create(signatureData.getV()));
            result.add(RlpString.create(Bytes.trimLeadingZeroes(signatureData.getR())));
            result.add(RlpString.create(Bytes.trimLeadingZeroes(signatureData.getS())));
        }

        return result;
    }
}
